Skip to content

Incremental statestream checkpoints for replays#18213

Merged
LibretroAdmin merged 20 commits intolibretro:masterfrom
JoeOsborn:bsv-movie-incremental-checkpoint
Aug 30, 2025
Merged

Incremental statestream checkpoints for replays#18213
LibretroAdmin merged 20 commits intolibretro:masterfrom
JoeOsborn:bsv-movie-incremental-checkpoint

Conversation

@JoeOsborn
Copy link
Copy Markdown
Contributor

With this patch, a series of dozens or hundreds of states can take up less space
than a single uncompressed state. This makes it feasible to take one
checkpoint per minute or even one per second, mitigating determinacy
issues in many cores.

Cores with higher memory usage may take more than one frame to
serialize or unserialize, which can lead to hitches during recording
and playback. This may require optimizing the encoding routine,
per-core optimizations to serialization, or tracking dirty regions of
memory and adding new API to serialize/deserialize just the changed
memory areas. As a reference, ppsspp saves can be encoded or decoded in roughly 10ms.

Desyncs may occur on deserialize (some cores, e.g. fceumm, exhibit this behavior; my guess is that these cores clear inputs on deserialize or something) .

  • Move input_driver.c bsvmovie code into new bsvmovie.c
    • New module: hashmap-based uint32s index for block-based deduplication
  • Bump replay format version to 2, including a bigger header (9 ints now),
    and making the initial savestate a "CHECKPOINT2"-type checkpoint
    (which holds compression and encoding info).
    • The new header fields are frame count, superblock size, and block
      size (the latter two are parameters on savestreams, per
      https://github.com/sumitshetye2/v86_savestreams/ )
    • Each frame now also carries a backpointer (distance in bytes) to the beginning of the previous frame.

This technique is broadly complementary to delta-encoding, and in the
future I think they could be combined (delta encoding for small
changes, deduplicated block encoding when deltas become large or a lot
of time has passed). The current checkpoint2 format can support
checkpoints using different encoding schemes and compression codecs,
so this shouldn't pose any difficulty down the road.

This also gives us a way (eventually) to rewind replays arbitrarily far
back (e.g., checkpoint-by-checkpoint), without relying on the rewind buffer. That will be in a later PR.

One config flag is added: replay_checkpoint_deserialize, enabled by default (the current state), which can be disabled for cores like fceumm which do not like to have states loaded during replay playback.

JoeOsborn and others added 15 commits August 28, 2025 11:16
Now, a series of dozens or hundreds of states can take up less space
than a single uncompressed state.  This makes it feasible to take one
checkpoint per minute or even one per second, mitigating determinacy
issues in many cores.

Cores with higher memory usage may take more than one frame to
serialize or unserialize, which can lead to hitches during recording
and playback.  Hitches or desyncs may also occur if inputs are cleared
on deserialize.  This may require optimizing the encoding routine,
per-core optimizations to serialization, or tracking dirty regions of
memory and adding new API to serialize/deserialize just the changed
memory areas.

- Move input_driver.c bsvmovie code into new bsvmovie.c
  - New module: hashmap-based uint32s index for block-based deduplication
- Bump replay format version to include a bigger header (9 ints now),
  and to make the initial savestate a "CHECKPOINT2"-type checkpoint
  (which holds compression and encoding info).
  - The new header fields are frame count, superblock size, and block
    size (the latter two are parameters on savestreams, per
    https://github.com/sumitshetye2/v86_savestreams/ )

This technique is broadly complementary to delta-encoding, and in the
future I think they could be combined (delta encoding for small
changes, deduplicated block encoding when deltas become large or a lot
of time has passed).  The current checkpoint2 format can support
checkpoints using different encoding schemes and compression codecs,
so this shouldn't pose any difficulty down the road.

This also gives us a way (eventually) to rewind replays arbitrarily far
back, without relying on the rewind buffer.  That will be some later
changes to state_manager.c.
- lower zstd compression level (3)
- higher hashmap capacity in savestream index
- bigger block size for savestreams
- use xxhash for savestream index
- track counts and print histogram (in debug mode) to understand index usage
…ol which is also set to false after rewinds or state loads, for use during both recording and replay.
Also tune settings a bit more for large memories

Set checkpoint compression based on savestate compression flag
@JoeOsborn
Copy link
Copy Markdown
Contributor Author

JoeOsborn commented Aug 28, 2025

Here are the results of my benchmarking; as I see it, there is very little time overhead for incremental statestream encoding. These are recorded on a plugged-in laptop with an AMD Ryzen 7 6800U inside.

| core         | game            | condition       | wall record time               | uncapped record time         | file size   | zstd -22 file size | wall playback time             | uncapped playback time       | ss blocks | reused | skipped | checks | distinct | hashes | superblocks | reused | distinct | unenc size | enc size | net time  | net time (playback) |
|--------------+-----------------+-----------------+--------------------------------+------------------------------+-------------+--------------------+--------------------------------+------------------------------+-----------+--------+---------+--------+----------+--------+-------------+--------+----------+------------+----------+-----------+---------------------|
| fceumm       | smb             | no cp, no zstd  | r 1m0.025s, u 2.888s, s 1.904s | r 3.903s, u 1.294s, s 0.641s | 128924 B    | 916 B              | r 1m0.028s, u 2.917s, s 1.997s | r 2.934s, u 1.344s, s 0.698s |           |        |         |        |          |        |             |        |          |            |          |           |                     |
| snes9x       | star fox        | no cp, no zstd  | r 1m0.072s, u 7.166s, s 1.947s | r 4.915s, u 3.074s, s 0.754s | 910828 B    | 7758 B             | r 59.971s, u 7.077s, s 1.823s  | r 6.091s, u 3.961s, s 0.774s |           |        |         |        |          |        |             |        |          |            |          |           |                     |
| pcsx_rearmed | megaman legends | no cp, no zstd  | r 1m0.079s, u 3.794s, s 2.168s | r 3.756s, u 1.626s, s 0.839s | 4629214 B   | 94183 B            | r 1m0.052s, u 3.806s, s 2.185s | r 3.739s, u 1.952s, s 0.861s |           |        |         |        |          |        |             |        |          |            |          |           |                     |
| fceumm       | smb             | no cp, zstd     | r 1m0.039s, u 2.930s, s 1.837s | r 2.861s, u 1.313s, s 0.702s | 115717 B    | 884 B              | r 1m0.002s, u 2.891s, s 1.980s | r 3.060s, u 1.431s, s 0.688s |           |        |         |        |          |        |             |        |          |            |          |           |                     |
| snes9x       | star fox        | no cp, zstd     | r 1m0.063s, u 7.288s, s 2.010s | r 5.211s, u 3.759s, s 0.677s | 93631 B     | 8176 B             | r 1m0.064s, u 7.367s, s 1.909s | r 5.674s, u 4.071s, s 0.713s |           |        |         |        |          |        |             |        |          |            |          |           |                     |
| pcsx_rearmed | megaman legends | no cp, zstd     | r 1m0.045s, u 3.666s, s 2.175s | r 3.867s, u 1.917s, s 0.869s | 279859 B    | 107505 B           | r 1m0.044s, u 3.571s, s 2.022s | r 3.482s, u 1.891s, s 0.806s |           |        |         |        |          |        |             |        |          |            |          |           |                     |
| fceumm       | smb             | raw cp, no zstd | r 1m0.024s, u 2.921s, s 2.121s | r 3.182s, u 1.409s, s 0.705s | 939584 B    | 22374 B            | r 1m0.036s, u 2.867s, s 2.113s | r 2.714s, u 1.375s, s 0.662s |           |        |         |        |          |        |             |        |          |            |          |           |                     |
| snes9x       | star fox        | raw cp, no zstd | r 1m0.069s, u 7.219s, s 2.007s | r 5.936s, u 4.142s, s 0.800s | 49552080 B  | 390230 B           | r 59.998s, u 7.256s, s 1.933s  | r 5.432s, u 4.057s, s 0.737s |           |        |         |        |          |        |             |        |          |            |          |           |                     |
| pcsx_rearmed | megaman legends | raw cp, no zstd | r 1m0.053s, u 3.834s, s 2.463s | r 4.215s, u 2.027s, s 1.088s | 267560472 B | 3618291 B          | r 1m0.024s, u 3.921s, s 2.244s | r 4.030s, u 2.071s, s 0.877s |           |        |         |        |          |        |             |        |          |            |          |           |                     |
| fceumm       | smb             | raw cp, zstd    | r 1m0.047s, u 2.966s, s 2.039s | r 3.245s, u 1.370s, s 0.711s | 208684 B    | 92931 B            | r 1m0.030s, u 2.919s, s 2.029s | r 2.891s, u 1.360s, s 0.730s |           |        |         |        |          |        |             |        |          |            |          |           |                     |
| snes9x       | star fox        | raw cp, zstd    | r 1m0.070s, u 7.253s, s 1.861s | r 6.084s, u 4.126s, s 0.781s | 4480651 B   | 3411972 B          | r 59.987s, u 7.237s, s 1.968s  | r 5.922s, u 4.026s, s 0.729s |           |        |         |        |          |        |             |        |          |            |          |           |                     |
| pcsx_rearmed | megaman legends | raw cp, zstd    | r 1m0.044s, u 4.504s, s 2.093s | r 4.624s, u 2.489s, s 0.956s | 69106803 B  | 32342372 B         | r 1m0.080s, u 4.120s, s 2.115s | r 3.734s, u 2.145s, s 0.867s |           |        |         |        |          |        |             |        |          |            |          |           |                     |
| fceumm       | smb             | ss cp, no zstd  | r 1m0.031s, u 2.909s, s 2.150s | r 3.006s, u 1.327s, s 0.752s | 340188 B    | 32796 B            | r 1m0.044s, u 2.954s, s 1.969s | r 2.856s, u 1.404s, s 0.708s |      6720 |   5110 |    4522 |   6264 |     1611 |   1958 |         420 |    240 |      181 | 804 KB     | 218 KB   | 0.009766s | 0.010093s           |
| snes9x       | star fox        | ss cp, no zstd  | r 1m0.051s, u 6.980s, s 2.033s | r 5.221s, u 3.368s, s 0.783s | 2497398 B   | 566215 B           | r 59.968s, u 7.247s, s 1.998s  | r 5.609s, u 4.008s, s 0.726s |    386880 | 369662 |  349866 | 373578 |    17219 |  36594 |       24180 |  21162 |     3019 | 48305 KB   | 2406 KB  | 0.037005s | 0.034025s           |
| pcsx_rearmed | megaman legends | ss cp, no zstd  | r 1m0.046s, u 4.142s, s 2.377s | r 4.650s, u 2.043s, s 0.967s | 77588737 B  | 3607139 B          | r 1m0.074s, u 4.219s, s 2.360s | r 3.920s, u 2.126s, s 0.895s |     16320 |  11599 |   11040 |  15776 |     4722 |   5280 |        1020 |    350 |      671 | 261120 KB  | 75600 KB | 0.074380s | 0.081427s           |
| fceumm       | smb             | ss cp, zstd     | r 1m0.052s, u 2.889s, s 2.111s | r 2.879s, u 1.376s, s 0.722s | 213485 B    | 98383 B            | r 1m0.048s, u 2.992s, s 2.195s | r 2.772s, u 1.322s, s 0.750s |      6720 |   5046 |    4530 |   6264 |     1675 |   1950 |         420 |    240 |      181 | 804 KB     | 227 KB   | 0.011096s | 0.009476s           |
| snes9x       | star fox        | ss cp, zstd     | r 1m0.091s, u 7.135s, s 2.214s | r 5.088s, u 3.365s, s 0.766s | 853761 B    | 767721 B           | r 59.977s, u 6.974s, s 1.973s  | r 5.625s, u 3.966s, s 0.765s |    386880 | 369766 |  349759 | 373578 |    17115 |  36701 |       24180 |  21156 |     3025 | 48305 KB   | 2393 KB  | 0.036826s | 0.031792s           |
| pcsx_rearmed | megaman legends | ss cp, zstd     | r 1m0.070s, u 3.938s, s 2.200s | r 4.348s, u 2.223s, s 0.900s | 14960385 B  | 14292131 B         | r 1m0.083s, u 3.912s, s 2.424s | r 3.608s, u 1.899s, s 0.915s |     16320 |  11682 |   11028 |  15776 |     4639 |   5292 |        1020 |    351 |      670 | 261120 KB  | 74272 KB | 0.067047s | 0.074118s           |

@LibretroAdmin LibretroAdmin merged commit 5b6c93a into libretro:master Aug 30, 2025
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants